<?PHP
#
#   FILE:  SystemConfiguration_Test.php
#
#   Part of the ScoutLib application support library
#   Copyright 2022 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu
#
# @scout:phpstan
// phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses

use ScoutLib\Database;
use ScoutLib\SystemConfiguration;

/**
 *
 */
class SystemConfiguration_Test extends PHPUnit\Framework\TestCase
{
    # ---- SETUP -------------------------------------------------------------

    const DATE_TEST_VAL_ONE = "October 1 2015 4:56am";
    const DATE_TEST_VAL_TWO = "12:34pm Jan 4 2001";
    const DB_TABLE_NAME = "SystemConfigurationTest";

    /**
     * Save current settings for later restoration.
     */
    public static function setUpBeforeClass() : void
    {
        self::$SavedTableName = SystemConfiguration::dbTableName();
        self::$SavedFields = SystemConfiguration::fields();
    }

    /**
     * Restore saved settings
     */
    public static function tearDownAfterClass() : void
    {
        SystemConfiguration::dbTableName(self::$SavedTableName);
        SystemConfiguration::fields(self::$SavedFields);
    }

    /**
     * Create necessary database tables for testing the class.
     */
    public function setUp() : void
    {
        $DB = new Database();
        $DB->query("CREATE TABLE ".self::DB_TABLE_NAME." (Placeholder INT)");
        $DB->query("INSERT INTO ".self::DB_TABLE_NAME." SET Placeholder = 1");
        SystemConfiguration::dbTableName(self::DB_TABLE_NAME);
        SystemConfiguration::fields($this->SampleSettings);
    }

    /**
     * Destroy tables that were created for testing.
     */
    public function tearDown() : void
    {
        $DB = new Database();
        $DB->query("DROP TABLE ".self::DB_TABLE_NAME);
    }

    # ---- TESTS -------------------------------------------------------------

    /**
     * Check that constructor throws exception when specified database table
     * does not exist.
     */
    public function testConstructorMissingDatabaseTable()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("Database table");
        SystemConfiguration::dbTableName(self::DB_TABLE_NAME."_NONEXISTENT");
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when a setting is specified
     * with no type.
     */
    public function testConstructorNoSettingType()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("No type specified");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        unset($CfgSettings["BadSetting"]["Type"]);
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when a setting is specified
     * with an invalid type.
     */
    public function testConstructorInvalidSettingType()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("Invalid type specified");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        $CfgSettings["BadSetting"]["Type"] = "BAD TYPE VALUE";
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when a setting is specified
     * with no default value.
     */
    public function testConstructorNoSettingDefault()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("No default specified");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        unset($CfgSettings["BadSetting"]["Default"]);
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when a setting is specified
     * with no description.
     */
    public function testConstructorNoSettingDescription()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("No description specified");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        unset($CfgSettings["BadSetting"]["Description"]);
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when an empty set of
     * valid values is specified.
     */
    public function testConstructorEmptyValidValuesSet()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("no entries");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        $CfgSettings["BadSetting"]["ValidValues"] = [];
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when a default value is
     * specified that is not in the list of valid values.
     */
    public function testConstructorDefaultNotInValidValueList()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("not in list of valid");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        $CfgSettings["BadSetting"]["ValidValues"] = [ 1, 2, 4 ];
        $CfgSettings["BadSetting"]["Default"] = 3;
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when a default value is
     * specified that is below the minimum value.
     */
    public function testConstructorDefaultBelowMinimumValue()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("below minimum value");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        $CfgSettings["BadSetting"]["MinVal"] = 4;
        $CfgSettings["BadSetting"]["Default"] = 3;
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that constructor throws exception when a default value is
     * specified that is above the maximum value.
     */
    public function testConstructorDefaultAboveMaximumValue()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage("above maximum value");
        $CfgSettings = $this->SampleSettings;
        $CfgSettings["BadSetting"] = $this->SampleSetting;
        $CfgSettings["BadSetting"]["MaxVal"] = 4;
        $CfgSettings["BadSetting"]["Default"] = 5;
        SystemConfiguration::fields($CfgSettings);
        $SysCfg = SystemConfiguration::getInstance();
    }

    /**
     * Check that the expected default values are returned for fields that
     * have not been set.
     */
    public function testDefaultValues()
    {
        $SysCfg = SystemConfiguration::getInstance();
        foreach ($this->SampleSettings as $SettingName => $SettingInfo) {
            switch ($SettingInfo["Type"]) {
                case SystemConfiguration::TYPE_ARRAY:
                    $this->assertEquals(
                        $SettingInfo["Default"],
                        $SysCfg->getArray($SettingName)
                    );
                    break;

                case SystemConfiguration::TYPE_BOOL:
                    $this->assertEquals(
                        $SettingInfo["Default"],
                        $SysCfg->getBool($SettingName)
                    );
                    break;

                case SystemConfiguration::TYPE_DATETIME:
                    $this->assertEquals(
                        strtotime($SettingInfo["Default"]),
                        $SysCfg->getDatetime($SettingName)
                    );
                    break;

                case SystemConfiguration::TYPE_FLOAT:
                    $this->assertEquals(
                        $SettingInfo["Default"],
                        $SysCfg->getFloat($SettingName)
                    );
                    break;

                case SystemConfiguration::TYPE_INT:
                    $this->assertEquals(
                        $SettingInfo["Default"],
                        $SysCfg->getInt($SettingName)
                    );
                    break;

                case SystemConfiguration::TYPE_STRING:
                    $this->assertEquals(
                        $SettingInfo["Default"],
                        $SysCfg->getString($SettingName)
                    );
                    break;
            }
        }
    }

    /**
     * Test that values of each supported type are stored and retrieved
     * correctly.
     */
    public function testGetSetValues()
    {
        $SysCfg = SystemConfiguration::getInstance();
        $TestDataSets = [ [ "Type" => "Array", "Value" => [ "A", "B", "C", 4, 5, 6 ] ],
            [ "Type" => "Bool", "Value" => true ],
            [ "Type" => "Datetime", "Value" => strtotime(self::DATE_TEST_VAL_TWO) ],
            [ "Type" => "Float", "Value" => 98765.4 ],
            [ "Type" => "Int", "Value" => 31415 ],
            [ "Type" => "String", "Value" => "What, me worry?" ],
        ];
        foreach ($TestDataSets as $DataSet) {
            $GetCallback = [$SysCfg, "get".$DataSet["Type"]];
            $SetCallback = [$SysCfg, "set".$DataSet["Type"]];
            $FieldName = $DataSet["Type"]."SettingOne";
            call_user_func($SetCallback, $FieldName, $DataSet["Value"]);
            $this->assertSame(
                $DataSet["Value"],
                call_user_func($GetCallback, $FieldName)
            );
        }
    }

    /**
     * Test that values can be unset and check for being set works.
     */
    public function testSetCheckAndUnset()
    {
        $SysCfg = SystemConfiguration::getInstance();
        $TestValues = [
            "IntSettingOne" => 1,
        ];
        $TestMethods = [
            "IntSettingOne" => "setInt",
        ];

        foreach ($TestValues as $FieldName => $FieldValue) {
            $this->assertFalse(
                $SysCfg->isSet($FieldName),
                "Setting \"".$FieldName."\" is reported as set when it has not been set."
            );
            call_user_func(
                [ $SysCfg, $TestMethods[$FieldName] ],
                $FieldName,
                $FieldValue
            );
            $this->assertTrue(
                $SysCfg->isSet($FieldName),
                "Setting \"".$FieldName."\" is reported as not set when it has been set."
            );
            $SysCfg->unset($FieldName);
            $this->assertFalse(
                $SysCfg->isSet($FieldName),
                "Setting \"".$FieldName."\" is reported as set when it has been unset."
            );
        }
    }

    # ---- PRIVATE -----------------------------------------------------------

    private $SampleSettings = [
        "ArraySettingOne" => [
            "Type" => SystemConfiguration::TYPE_ARRAY,
            "Default" => [ 1, 2, 3 ],
            "Description" => "ABC is easy as 123.",
        ],
        "BoolSettingOne" => [
            "Type" => SystemConfiguration::TYPE_BOOL,
            "Default" => false,
            "Description" => "ABC is easy as 123.",
        ],
        "DatetimeSettingOne" => [
            "Type" => SystemConfiguration::TYPE_DATETIME,
            "Default" => self::DATE_TEST_VAL_ONE,
            "Description" => "ABC is easy as 123.",
        ],
        "FloatSettingOne" => [
            "Type" => SystemConfiguration::TYPE_FLOAT,
            "Default" => 123.456,
            "Description" => "ABC is easy as 123.",
        ],
        "IntSettingOne" => [
            "Type" => SystemConfiguration::TYPE_INT,
            "Default" => 0,
            "Description" => "ABC is easy as 123.",
        ],
        "StringSettingOne" => [
            "Type" => SystemConfiguration::TYPE_STRING,
            "Default" => "Four score and seven years ago.",
            "Description" => "ABC is easy as 123.",
        ],
    ];

    private $SampleSetting = [
        "Type" => SystemConfiguration::TYPE_INT,
        "Default" => 0,
        "Description" => "This is another setting.",
    ];

    private static $SavedTableName;
    private static $SavedFields;
}
